/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2011  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.ui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.filechooser.FileFilter;

import org.sosy_lab.ccvisu.CCVisuController;
import org.sosy_lab.ccvisu.CCVisuOptions;
import org.sosy_lab.ccvisu.InformationProvider;
import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.readers.InputFormat;
import org.sosy_lab.ccvisu.readers.ReaderData;
import org.sosy_lab.ccvisu.readers.ReaderDataLayoutLAY;
import org.sosy_lab.ccvisu.ui.FrameDisplay.GraphCanvas;
import org.sosy_lab.ccvisu.ui.FrameDisplay.LoadDirection;
import org.sosy_lab.ccvisu.ui.controlpanel.OptionsPanelVisualization;
import org.sosy_lab.ccvisu.writers.OutputFormat;
import org.sosy_lab.ccvisu.writers.WriterData;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutDISP;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutLAY;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutSVG;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutVRML;
import org.sosy_lab.common.LogManager;
import org.sosy_lab.common.configuration.Configuration;
import org.sosy_lab.common.configuration.InvalidConfigurationException;
import org.sosy_lab.util.interfaces.ProgressReceiver;
import org.sosy_lab.util.interfaces.Progressing;
import org.sosy_lab.util.interfaces.WorkerManager;

/**
 * The application's main frame.
 */
public class FrameMain extends JFrame implements ProgressReceiver, WorkerManager {

  private static final long     serialVersionUID            = -4778809967193305287L;

  private JPanel                mainContainer               = new JPanel();

  private FrameMenuBar          menuBar;

  private JTextField            searchTextField             = new JTextField();
  private JProgressBar          progressBar                 = new JProgressBar();
  private JButton               cancelButton                = new JButton("Stop");
  private ExecutorService       executorService             = null;

  private final FrameTools      propertiesWindow;
  private final CCVisuOptions   options;
  private JDialog               optionsDialog;

  private JPanel                bottomPanel;
  private JPanel                searchPanel;
  private JInternalFrame        graphLayoutFrame;
  private PrintWriter           printer;
  private CCVisuController      controller;
  private WriterDataLayoutDISP  writer;
  private GraphCanvas           canvas;
  private FrameDisplay          display;

  private final Configuration   config;
  private final LogManager      logger;

  public JComponent getCanvasContainer() {
    return this.mainContainer;
  }

  /**
   * Set the window title. Include the name of the active file.
   * @param pActiveFileName
   */
  private void updateWindow() {
    File inputFile = new File(controller.getInputName());
    setTitle("CCVisu | www.sosy-lab.org | ".concat(inputFile.getName()));

    if (canvas != null) {
      canvas.updateAndPaint();
    }
  }

  public FrameMain(CCVisuController ccvisuController, CCVisuOptions options,
      JInternalFrame graphLayoutFrame, Configuration config, LogManager logger)
          throws InvalidConfigurationException {

    super("CCVisu");

    // set fields defined by parametes
    this.options = options;
    this.graphLayoutFrame = graphLayoutFrame;
    this.controller = ccvisuController;
    this.config = config;
    this.logger = logger;

    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    setSize(1280, 768);

    // create printer and writer
    printer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
    writer = new WriterDataLayoutDISP(printer, options.getGraph(),
        graphLayoutFrame, options, controller, config, logger);

    // set display and canvas
    display = writer.getDisplay();
    canvas = display.getCanvas();

    initComponents();

    propertiesWindow = new FrameTools(options, logger, writer, this,
        ccvisuController, config);
    propertiesWindow.setVisible(true);
    add(propertiesWindow, BorderLayout.EAST);

    addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent evt) {
        //propertiesWindow.dispose();
      }
    });

    updateWindow();
  }

  private void initComponents() {
    searchPanel = createSearchPanel();
    bottomPanel = createBottomPanel();

    mainContainer.setBorder(javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4));
    mainContainer.setLayout(new BorderLayout());
    mainContainer.add(searchPanel, BorderLayout.NORTH);
    mainContainer.add(graphLayoutFrame, BorderLayout.CENTER);
    mainContainer.add(bottomPanel, BorderLayout.SOUTH);

    menuBar = new FrameMenuBar(this, writer, options.getGraph());

    setLayout(new BorderLayout());
    add(mainContainer, BorderLayout.CENTER);
    setJMenuBar(menuBar);

    searchTextField.addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
          if (searchTextField.getText().isEmpty()) {
            writer.resetMarkedVertices();
          } else {
            writer.markVertices(".*" + searchTextField.getText() + ".*");
          }
          canvas.updateAndPaint();
        }
      }
    });
    cancelButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent pE) {
        cancelActiveWorkerThreads();
      }
    });
  }

  private JPanel createBottomPanel() {
    JPanel bottomPanel = new JPanel();

    bottomPanel.setLayout(new BorderLayout());
    bottomPanel.add(progressBar, BorderLayout.CENTER);
    bottomPanel.add(cancelButton, BorderLayout.EAST);
    bottomPanel.setEnabled(false);
    bottomPanel.setVisible(false);

    return bottomPanel;
  }

  private JPanel createSearchPanel() {
    JPanel searchPanel = new JPanel();

    searchPanel.setLayout(new BorderLayout());
    searchPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(3, 0, 3, 0));
    searchPanel.add(new JLabel("Search for node: "), BorderLayout.WEST);
    searchPanel.add(searchTextField, BorderLayout.CENTER);

    return searchPanel;
  }

  void openSaveLayoutDialog() {
    JFileChooser fileDialog = new JFileChooser(".");
    fileDialog.setFileFilter(ReaderData.mkExtensionFileFilter(OutputFormat.LAY));
    int outcome = fileDialog.showSaveDialog(canvas.getParentFrame());

    if (outcome == JFileChooser.APPROVE_OPTION) {
      assert (fileDialog.getCurrentDirectory() != null);
      assert (fileDialog.getSelectedFile() != null);

      // write file layout
      writeFileLayout(fileDialog.getCurrentDirectory().toString()
          + File.separator + fileDialog.getSelectedFile().getName());
    }
  }

  void showOrHideControlPanel() {
    propertiesWindow.setVisible(!propertiesWindow.isVisible());
  }

  void openLoadLayoutDialog() {
    JFileChooser fileDialog = new JFileChooser(".");
    fileDialog.setFileFilter(ReaderData.mkExtensionFileFilter(InputFormat.LAY));
    int outcome = fileDialog.showOpenDialog(canvas.getParentFrame());

    if (outcome == JFileChooser.APPROVE_OPTION) {
      assert (fileDialog.getCurrentDirectory() != null);
      assert (fileDialog.getSelectedFile() != null);

      loadFile(fileDialog.getCurrentDirectory().toString()
          + File.separator + fileDialog.getSelectedFile().getName());
    }
  }


  void openVisualOptionsDialog() {
    if (this.optionsDialog != null) {
      this.optionsDialog.setVisible(true);
    } else {

      optionsDialog = new JDialog(this, "Options");

      JPanel panel = new JPanel();
      panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
      panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
      panel.add(new OptionsPanelVisualization(options, writer, controller));

      JPanel buttonPane = new JPanel();
      buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
      buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
      buttonPane.add(Box.createHorizontalGlue());

      JButton okButton = new JButton("Ok");
      okButton.setMnemonic('o');
      okButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          optionsDialog.setVisible(false);
        }
      });

      buttonPane.add(okButton);
      buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
      panel.add(buttonPane);

      optionsDialog.add(panel);
      optionsDialog.pack();
      optionsDialog.setResizable(false);
      okButton.requestFocusInWindow();
      optionsDialog.setVisible(true);
    }
  }

  void resetZoom() {
    canvas.zoomOut();
  }

  void openLoadDialog() {
    // Setup filechooser.
    JFileChooser fileChooser = new JFileChooser(".");
    fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);

    FileFilter defaultFilter = null;
    for (final InputFormat inputFormat : InputFormat.values()) {
      FileFilter filter = new FileFilter() {
        @Override
        public String getDescription() {
          return inputFormat.getDescription();
        }

        @Override
        public boolean accept(File file) {
          return (file.isDirectory()
              || file.getAbsoluteFile().getName().toLowerCase().endsWith(inputFormat.getFileExtension()));
        }
      };

      fileChooser.addChoosableFileFilter(filter);
      if (defaultFilter == null || inputFormat == InputFormat.RSF) {
        defaultFilter = filter;
      }
    }
    fileChooser.setFileFilter(defaultFilter);

    // Show filechooser.
    if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
      String fileName = fileChooser.getSelectedFile().getAbsoluteFile().getAbsolutePath();
      controller.setInputFormat(InputFormat.getInFormatOfFile(fileName));
      controller.setInputName(fileName);

      try {
        // Load the choosen file.
        controller.processVisual(options.getGraph());

        // Disable the file load menu entry, because CCVisu does currently
        // not support loading multiple files in one session.
        menuBar.disableLoadMenuItem();
        this.updateWindow();
      } catch (InvalidConfigurationException e) {
        e.printStackTrace();
      } catch (IOException e) {
        JOptionPane.showMessageDialog(this, "Opening file failed: " + e.getMessage(),
            "Error", JOptionPane.ERROR_MESSAGE);
        e.printStackTrace();
      }
    }
  }

  void openSaveDialog() {
    JFileChooser fileChooser = new JFileChooser(".");
    fileChooser.setFileSelectionMode(JFileChooser.SAVE_DIALOG);

    for (final OutputFormat outputFormat : OutputFormat.values()) {
      switch (outputFormat) {
      case DISP:
      case NULL:
        break;
      default:
        fileChooser.addChoosableFileFilter(new FileFilter() {
          @Override
          public String getDescription() {
            return outputFormat.getDescription();
          }

          @Override
          public boolean accept(File file) {
            return (file.isDirectory()
                || file.getAbsoluteFile().getName().toLowerCase().endsWith(outputFormat.getFileExtension()));
          }

          @Override
          public String toString() {
            return outputFormat.getFileExtension();
          }
        });
      }
    }

    if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
      String fileName = fileChooser.getSelectedFile().getAbsoluteFile().getAbsolutePath();
      OutputFormat outFormat = OutputFormat.getOutFormatByExtension(fileChooser.getFileFilter().toString());

      try {
        controller.writeOutput(fileName, outFormat, options);
        JOptionPane.showMessageDialog(this, "File successfully written!",
            "Done", JOptionPane.INFORMATION_MESSAGE);

      } catch (IOException e) {
        JOptionPane.showMessageDialog(this, "Writing file failed: " + e.getMessage(),
            "Error", JOptionPane.ERROR_MESSAGE);
        e.printStackTrace();
      }
    }
  }

  void openAboutDialog() {
    JOptionPane.showMessageDialog(this, InformationProvider.versionMessage(),
        "About CCVisu", JOptionPane.INFORMATION_MESSAGE);
  }

  /**
   * Load next/previous layout from file,
   * to leaf through the directory file by file.
   * @param loadDirection  determines whether next or previous file
   *                       should be loaded.
   */
  void loadOtherFile(LoadDirection loadDirection) {
    final String fileName = controller.getInputName();
    String path = ".";

    if (fileName.lastIndexOf(File.separator) != -1) {
      path = fileName.substring(0, fileName.lastIndexOf(File.separator));
    }

    String[] fileList = (new File(path)).list(new FilenameFilter() {
      @Override
      public boolean accept(File dir, String name) {
        return name.endsWith(".lay");
      }
    });

    if (fileList.length == 0) {
      logger.log(Level.INFO, "No layout (.lay) file available in current directory.");
      return;
    }

    Arrays.sort(fileList);
    int fileCurrent = Arrays.binarySearch(fileList, new File(fileName).getName());

    if (fileCurrent < 0) { // File not found in current directory.
      fileCurrent = 0;

    } else if (loadDirection == LoadDirection.NEXT && (fileCurrent < fileList.length - 1)) {
      fileCurrent++;

    } else if (loadDirection == LoadDirection.PREV && (fileCurrent > 0)) {
      fileCurrent--;
    }

    controller.setInputName(path + File.separator + fileList[fileCurrent]);
    // Load next layout.
    loadFile(controller.getInputName());
  }

  private void loadFile(String fileName) {
    BufferedReader reader = null;
    GraphData layout = new GraphData(logger);

    try {
      reader = new BufferedReader(new FileReader(fileName));
    } catch (Exception e) {
      logger.logDebugException(e, "Exception while opening file '" + fileName
          + "' for reading: ");
    }

    // Read layout from file.
    (new ReaderDataLayoutLAY(logger, reader)).read(layout);
    options.setGraph(layout);
    updateWindow();

    // Close the input file.
    try {
      reader.close();
    } catch (Exception e) {
      logger.logDebugException(e, "Exception while closing input file '"
          + fileName + "'");
    }
  }

  /**
   * Writes layout to file using an implementation of class <code>WriterData</code>.
   * Call-back method, invoked from within ScreenDisplay.
   * @param fileName     Name of the output file to write the layout to.
   */
  public void writeFileLayout(String fileName) {
    try {
      PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
      WriterData dataWriter = new WriterDataLayoutLAY(out, options.getGraph(),
          options, config); // Default, also .lay.

      if (fileName.endsWith(".svg")) {
        dataWriter = new WriterDataLayoutSVG(out, options.getGraph(), options,
            controller, config, controller.getIsShowAllLabels());
      } else if (fileName.endsWith(".wrl")) {
        dataWriter = new WriterDataLayoutVRML(out, options.getGraph(), options,
            controller, config);
      }

      dataWriter.write();
      out.flush();
      out.close();
      logger.log(Level.INFO, "Wrote layout to output file '" + fileName + "'.");

    } catch (Exception e) {
      logger.logDebugException(e, "Exception while writing file '"
          + fileName + "'");
    }
  }

  @Override
  public void processChange(final Object source, final int value, final int max,
      final String message) {

    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        progressBar.setMaximum(max);
        progressBar.setValue(value);

        progressBar.setToolTipText(message);
        //TODO: verify level
        logger.log(Level.FINE, message);

        progressBar.setEnabled(value != max);
        cancelButton.setEnabled(value != max);
        bottomPanel.setVisible(progressBar.isEnabled() || cancelButton.isEnabled());
        if (value == max) {
          progressBar.setValue(0);
        }
      }
    });
  }

  /**
   * Add a task that can provide progress information
   * to the executor service.
   */
  @Override
  public <T extends Runnable & Progressing> void addAndRunTask(T task, String name) {
    task.addProgressReceiver(this);

    synchronized (this) {
      if (executorService == null) {
        executorService = Executors.newFixedThreadPool(1);
      }
      executorService.execute(task);
    }
  }

  public void cancelActiveWorkerThreads() {
    synchronized (this) {
      if (executorService != null) {
        executorService.shutdownNow();
        try {
          executorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
        }
        executorService = null;
      }
    }

    this.processChange(this, 0, 0, "");
  }
}
